package cc.z8g.browser.core

import android.graphics.Bitmap
import android.widget.ImageView
import androidx.activity.result.ActivityResult
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity
import cc.z8g.browser.BrowserApp
import cc.z8g.browser.R
import cc.z8g.browser.core.BrowserConst.CloseTab
import cc.z8g.browser.core.config.CookieAdministrator
import cc.z8g.browser.core.config.KeyCombos
import cc.z8g.browser.core.config.MenuItems
import cc.z8g.browser.core.download.DownloadRequest
import cc.z8g.browser.core.search.SearchBoxModel
import cc.z8g.browser.core.tab.HomePageCreator
import cc.z8g.browser.core.tab.HtmlPageCreator
import cc.z8g.browser.core.tab.NoOpCreator
import cc.z8g.browser.core.tab.PermissionCreator
import cc.z8g.browser.core.tab.RecentTabModel
import cc.z8g.browser.core.tab.TabCreator
import cc.z8g.browser.core.tab.TabModel
import cc.z8g.browser.core.tab.TabPager
import cc.z8g.browser.core.tab.TabViewState
import cc.z8g.browser.core.tab.TabWebChromeClient
import cc.z8g.browser.core.tab.TabsRepository
import cc.z8g.browser.core.tab.UrlCreator
import cc.z8g.browser.core.tab.bundle.BundleStore
import cc.z8g.browser.core.tab.bundle.DefaultBundleStore
import cc.z8g.browser.core.tab.bundle.IncognitoBundleStore
import cc.z8g.browser.core.view.LongPress
import cc.z8g.browser.database.Bookmark
import cc.z8g.browser.database.HistoryEntry
import cc.z8g.browser.database.SearchSuggestion
import cc.z8g.browser.database.WebPage
import cc.z8g.browser.database.asFolder
import cc.z8g.browser.database.bookmark.BookmarkDAO
import cc.z8g.browser.database.history.HistoryDAO
import cc.z8g.browser.database.history.HistoryDAO.Companion.getImpl
import cc.z8g.browser.html.AbsListPageFactory.BOOKMARKS_FILE_NAME
import cc.z8g.browser.html.BookmarkFactory
import cc.z8g.browser.html.HistoryFactory
import cc.z8g.browser.html.WebResFactory
import cc.z8g.browser.search.suggestions.SearchEngineProvider
import cc.z8g.browser.ssl.SslState
import cc.z8g.browser.utils.Option
import cc.z8g.browser.utils.QUERY_PLACE_HOLDER
import cc.z8g.browser.utils.ThreadPool
import cc.z8g.browser.utils.isBookmarkUrl
import cc.z8g.browser.utils.isHistoryUrl
import cc.z8g.browser.utils.isInnerUrl
import cc.z8g.browser.utils.isWebResUrl
import cc.z8g.browser.utils.smartUrlFilter
import cc.z8g.browser.utils.value
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Scheduler
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.Observables
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.kotlin.toObservable
import java.util.concurrent.Callable
import kotlin.system.exitProcess

/**
 * Presenter控制着浏览器UI的行为以及用户在默认和隐身浏览器中的交互。
 * 这个Presenter应该在整个浏览器活动期间存在，而浏览器本身在配置更改期间不应该重新创建。
 */
class BrowserPresenter(
    val activity: BrowserActivity,
    fragmentActivity: FragmentActivity,
    tabPager: TabPager,
    initialUrl: String?,
    private val incognitoMode: Boolean
) {
    private val iconFreeze: Bitmap = ContextCompat.getDrawable(activity, R.drawable.ic_frozen)!!.toBitmap()

    private val databaseScheduler: Scheduler = ThreadPool.getDBScheduler();
    private val mainScheduler: Scheduler = ThreadPool.getMainScheduler();

    private var navigator = BrowserNavigator(fragmentActivity)
    private val mHistoryFactory = HistoryFactory(activity)
    private val mBookmarkFactory = BookmarkFactory(activity)
    private val historyCreator = HtmlPageCreator(HistoryFactory(activity))
    private val webResCreator = HtmlPageCreator(WebResFactory(activity, this))
    private val bookmarkCreator = HtmlPageCreator(BookmarkFactory(activity))
    private val mHomePageCreator = HomePageCreator(bookmarkCreator)
    private var bundleStore: BundleStore = if (incognitoMode) IncognitoBundleStore else {
        DefaultBundleStore(BrowserApp.getApplication(), mHomePageCreator, bookmarkCreator, historyCreator, webResCreator)
    }

    private val permissionInitializerFactory: PermissionCreator.Factory = object : PermissionCreator.Factory {
        override fun create(url: String): PermissionCreator {
            return PermissionCreator(url, activity, mHomePageCreator)
        }
    }


    private val tabWebChromeClientProvider = Callable { TabWebChromeClient(fragmentActivity) }

    private var model = TabsRepository(activity, incognitoMode, tabPager, bundleStore, RecentTabModel(), initialUrl,
        permissionInitializerFactory, iconFreeze, tabWebChromeClientProvider)

    private var view: BrowserContract.View? = null
    private var viewState: BrowserViewState = BrowserViewState(
        displayUrl = "",
        isRefresh = true,
        sslState = SslState.None,
        progress = 0,
        enableFullMenu = true,
        themeColor = Option.None,
        isForwardEnabled = false,
        isBackEnabled = false,
        bookmarks = emptyList(),
        isBookmarked = false,
        isBookmarkEnabled = true,
        isRootFolder = true,
        findInPage = ""
    )
    private var tabListState: List<TabViewState> = emptyList()
    var currentTab: TabModel? = null
    private var currentFolder: Bookmark.Folder = Bookmark.Folder.Root
    private var isTabDrawerOpen = false
    private var isBookmarkDrawerOpen = false
    private var isSearchViewFocused = false
    private var tabIdOpenedFromAction = -1
    private var pendingAction: BrowserContract.Action.LoadUrl? = null
    private var isCustomViewShowing = false

    private val compositeDisposable = CompositeDisposable()
    private val allTabsDisposable = CompositeDisposable()
    private var tabDisposable: CompositeDisposable = CompositeDisposable()

    /**
     * Call when the view is attached to the presenter.
     */
    fun onViewAttached(view: BrowserContract.View) {
        this.view = view
        view.updateState(viewState)

        CookieAdministrator.adjustCookieSettings(incognitoMode);

        currentFolder = Bookmark.Folder.Root
        compositeDisposable += BookmarkDAO.getImpl().bookmarksAndFolders(folder = Bookmark.Folder.Root)
            .subscribeOn(databaseScheduler)
            .observeOn(mainScheduler)
            .subscribe { list ->
                this.view?.updateState(viewState.copy(bookmarks = list, isRootFolder = true))
            }

        compositeDisposable += model.tabsListChanges()
            .observeOn(mainScheduler)
            .subscribe { list ->
                this.view?.updateTabs(list.map { it.asViewState() })

                allTabsDisposable.clear()
                list.subscribeToUpdates(allTabsDisposable)
//                IncognitoNotification.notifyTabCountChange(list.size, view.getActivity())
            }

        compositeDisposable += model.initializeTabs()
            .observeOn(mainScheduler)
            .switchIfEmpty(model.createTab(mHomePageCreator).map(::listOf))
            .subscribe { list ->
                selectTab(model.selectTab(list.last().id))
            }
    }

    /**
     * Call when the view is detached from the presenter.
     */
    fun onViewDetached() {
        view = null

        compositeDisposable.dispose()
        tabDisposable.dispose()
    }

    /**
     * Call when the view is hidden (i.e. the browser is sent to the background).
     */
    fun onViewHidden() {
        model.freeze()
        tabIdOpenedFromAction = -1
    }

    private fun TabModel.asViewState(): TabViewState = TabViewState(
        id = id,
        icon = favicon,
        title = title,
        isSelected = isForeground
    )

    private fun List<TabViewState>.updateId(
        id: Int,
        map: (TabViewState) -> TabViewState
    ): List<TabViewState> = map {
        if (it.id == id) {
            map(it)
        } else {
            it
        }
    }

    private fun selectTab(tabModel: TabModel?) {
        if (currentTab == tabModel) {
            return
        }
        currentTab?.isForeground = false
        currentTab = tabModel
        currentTab?.isForeground = true

        view?.clearSearchFocus()

        val tab = tabModel ?: return run {
            view.updateState(
                viewState.copy(
                    displayUrl = SearchBoxModel.getDisplayContent(
                        url = "",
                        title = null,
                        isLoading = false
                    ),
                    enableFullMenu = false,
                    isForwardEnabled = false,
                    isBackEnabled = false,
                    sslState = SslState.None,
                    progress = 100,
                    findInPage = ""
                )
            )
            view.updateTabs(tabListState.map { it.copy(isSelected = false) })
        }

        view?.showToolbar()
        view?.closeTabDrawer()

        view.updateTabs(tabListState.map { it.copy(isSelected = it.id == tab.id) })

        tabDisposable.dispose()
        tabDisposable = CompositeDisposable()
        tabDisposable += Observable.combineLatest(
            tab.sslChanges().startWithItem(tab.sslState),
            tab.titleChanges().startWithItem(tab.title),
            tab.urlChanges().startWithItem(tab.url),
            tab.loadingProgress().startWithItem(tab.loadingProgress),
            tab.canGoBackChanges().startWithItem(tab.canGoBack()),
            tab.canGoForwardChanges().startWithItem(tab.canGoForward()),
            tab.urlChanges().startWithItem(tab.url).observeOn(ThreadPool.getDBScheduler())
                .flatMapSingle(BookmarkDAO.getImpl()::isBookmark).observeOn(mainScheduler),
            tab.urlChanges().startWithItem(tab.url).map(String::isInnerUrl),
            tab.themeColorChanges().startWithItem(tab.themeColor)
        ) { sslState, title, url, progress, canGoBack, canGoForward, isBookmark, isSpecialUrl, themeColor ->
            viewState.copy(
                displayUrl = SearchBoxModel.getDisplayContent(
                    url = url,
                    title = title,
                    isLoading = progress < 100
                ).takeIf { !isSearchViewFocused } ?: viewState.displayUrl,
                enableFullMenu = !url.isInnerUrl(),
                themeColor = Option.Some(themeColor),
                isRefresh = (progress == 100).takeIf { !isSearchViewFocused }
                    ?: viewState.isRefresh,
                isForwardEnabled = canGoForward,
                isBackEnabled = canGoBack,
                sslState = sslState.takeIf { !isSearchViewFocused } ?: viewState.sslState,
                progress = progress,
                isBookmarked = isBookmark,
                isBookmarkEnabled = !isSpecialUrl,
                findInPage = tab.findQuery.orEmpty()
            )
        }.observeOn(mainScheduler)
            .subscribe { view.updateState(it) }

        tabDisposable += tab.downloadRequests()
            .subscribeOn(mainScheduler)
            .subscribeBy(onNext = navigator::download)

        tabDisposable += tab.urlChanges()
            .distinctUntilChanged()
            .subscribeOn(mainScheduler)
            .subscribeBy { view?.showToolbar() }

        tabDisposable += tab.createWindowRequests()
            .subscribeOn(mainScheduler)
            .subscribeBy { createNewTabAndSelect(it, shouldSelect = true) }

        tabDisposable += tab.closeWindowRequests()
            .subscribeOn(mainScheduler)
            .subscribeBy { onTabClose(tabListState.indexOfCurrentTab()) }

        tabDisposable += tab.fileChooserRequests()
            .subscribeOn(mainScheduler)
            .subscribeBy { view?.showFileChooser(it) }

        tabDisposable += tab.showCustomViewRequests()
            .subscribeOn(mainScheduler)
            .subscribeBy {
                view?.showCustomView(it)
                isCustomViewShowing = true
            }

        tabDisposable += tab.hideCustomViewRequests()
            .subscribeOn(mainScheduler)
            .subscribeBy {
                view?.hideCustomView()
                isCustomViewShowing = false
            }
    }

    private fun List<TabModel>.subscribeToUpdates(compositeDisposable: CompositeDisposable) {
        forEach { tabModel ->
            compositeDisposable += Observables.combineLatest(
                tabModel.titleChanges().startWithItem(tabModel.title),
                tabModel.faviconChanges()
                    .startWithItem(Option.fromNullable(tabModel.favicon))
            ).distinctUntilChanged()
                .subscribeOn(mainScheduler)
                .subscribeBy { (title, bitmap) ->
                    view.updateTabs(tabListState.updateId(tabModel.id) {
                        it.copy(title = title, icon = bitmap.value())
                    })

                    tabModel.url.takeIf { !it.isInnerUrl() && it.isNotBlank() }?.let {
                        if (!incognitoMode) { // 访问历史记录
                            getImpl().visitHistoryEntry(it, title).subscribeOn(ThreadPool.getDBScheduler()).subscribe()
                        }
                    }
                }
        }
    }

    /**
     * Call when a new action is triggered, such as the user opening a new URL in the browser.
     */
    fun onNewAction(action: BrowserContract.Action) {
        when (action) {
            is BrowserContract.Action.LoadUrl -> if (action.url.isInnerUrl()) {
                view?.showLocalFileBlockedDialog()
                pendingAction = action
            } else {
                createNewTabAndSelect(
                    tabCreator = UrlCreator(action.url),
                    shouldSelect = true,
                    markAsOpenedFromAction = true
                )
            }

            BrowserContract.Action.Panic -> panicClean()
        }
    }

    /**
     * Call when the user confirms that they do or do not want to allow a local file to be opened
     * in the browser. This is a security gate to prevent malicious local files from being opened
     * in the browser without the user's knowledge.
     */
    fun onConfirmOpenLocalFile(allow: Boolean) {
        if (allow) {
            pendingAction?.let {
                createNewTabAndSelect(
                    tabCreator = UrlCreator(it.url),
                    shouldSelect = true,
                    markAsOpenedFromAction = true
                )
            }
        }
        pendingAction = null
    }

    private fun panicClean() {
        createNewTabAndSelect(tabCreator = NoOpCreator(), shouldSelect = true)
        model.clean()

        mHistoryFactory.deleteHistoryPage().subscribe()
        model.deleteAllTabs().subscribe()
        navigator.closeBrowser()

        // System exit needed in the case of receiving
        // the panic intent since finish() isn't completely
        // closing the browser
        exitProcess(1)
    }

    /**
     * Call when the user selects an option from the menu.
     */
    fun onMenuClick(@MenuItems.MenuSelection menuSelection: Int) {
        when (menuSelection) {
            MenuItems.NEW_INCOGNITO_WINDOW -> navigator.launchIncognitoWindow(url = null)
            MenuItems.SHARE -> currentTab?.url?.takeIf { !it.isInnerUrl() }?.let {
                navigator.sharePage(url = it, title = currentTab?.title)
            }
            MenuItems.OPEN_HISTORY -> createNewTabAndSelect(historyCreator, shouldSelect = true)
            MenuItems.OPEN_RESOURCES -> createNewTabAndSelect(webResCreator, shouldSelect = true)
            MenuItems.OPEN_DOWNLOADS -> activity.openDownloads()
            MenuItems.FIND -> view?.showFindInPageDialog()
            MenuItems.COPY_LINK -> currentTab?.url?.takeIf { !it.isInnerUrl() }?.let(navigator::copyPageLink)

            MenuItems.ADD_BOOKMARK -> currentTab?.url?.takeIf { !it.isInnerUrl() }?.let { showAddBookmarkDialog() }

            MenuItems.SETTINGS -> navigator.openSettings()
            MenuItems.BACK -> onBackClick()
            MenuItems.FORWARD -> onForwardClick()
        }
    }

    private fun createNewTabAndSelect(
        tabCreator: TabCreator,
        shouldSelect: Boolean,
        markAsOpenedFromAction: Boolean = false
    ) {
        compositeDisposable += model.createTab(tabCreator)
            .observeOn(mainScheduler)
            .subscribe { tab ->
                if (shouldSelect) {
                    selectTab(model.selectTab(tab.id))
                    if (markAsOpenedFromAction) {
                        tabIdOpenedFromAction = tab.id
                    }
                }
            }
    }

    private fun List<TabViewState>.tabIndexForId(id: Int?): Int =
        indexOfFirst { it.id == id }

    private fun List<TabViewState>.indexOfCurrentTab(): Int = tabIndexForId(currentTab?.id)

    /**
     * Call when the user selects a combination of keys to perform a shortcut.
     */
    fun onKeyComboClick(keyCombo: Int) {
        when (keyCombo) {
            KeyCombos.CTRL_F -> view?.showFindInPageDialog()
            KeyCombos.CTRL_T -> onNewTabClick()
            KeyCombos.CTRL_W -> onTabClose(tabListState.indexOfCurrentTab())
            KeyCombos.CTRL_Q -> view?.showCloseBrowserDialog(tabListState.indexOfCurrentTab())
            KeyCombos.CTRL_R -> onRefreshOrStopClick()
            KeyCombos.CTRL_TAB -> TODO()
            KeyCombos.CTRL_SHIFT_TAB -> TODO()
            KeyCombos.SEARCH -> TODO()
            KeyCombos.ALT_0 -> onTabClick(0.coerceAtMost(tabListState.size - 1))
            KeyCombos.ALT_1 -> onTabClick(1.coerceAtMost(tabListState.size - 1))
            KeyCombos.ALT_2 -> onTabClick(2.coerceAtMost(tabListState.size - 1))
            KeyCombos.ALT_3 -> onTabClick(3.coerceAtMost(tabListState.size - 1))
            KeyCombos.ALT_4 -> onTabClick(4.coerceAtMost(tabListState.size - 1))
            KeyCombos.ALT_5 -> onTabClick(5.coerceAtMost(tabListState.size - 1))
            KeyCombos.ALT_6 -> onTabClick(6.coerceAtMost(tabListState.size - 1))
            KeyCombos.ALT_7 -> onTabClick(7.coerceAtMost(tabListState.size - 1))
            KeyCombos.ALT_8 -> onTabClick(8.coerceAtMost(tabListState.size - 1))
            KeyCombos.ALT_9 -> onTabClick(9.coerceAtMost(tabListState.size - 1))
        }
    }

    /**
     * Call when the user selects a tab to switch to at the provided [index].
     */
    fun onTabClick(index: Int) {
        selectTab(model.selectTab(tabListState[index].id))
    }

    /**
     * Call when the user long presses on a tab at the provided [index].
     */
    fun onTabLongClick(index: Int) {
        view?.showCloseBrowserDialog(tabListState[index].id)
    }

    private fun <T> List<T>.nextSelected(removedIndex: Int): T? {
        val nextIndex = when {
            size > removedIndex + 1 -> removedIndex + 1
            removedIndex > 0 -> removedIndex - 1
            else -> -1
        }
        return if (nextIndex >= 0) {
            this[nextIndex]
        } else {
            null
        }
    }

    /**
     * Call when the user clicks on the close button for the tab at the provided [index]
     */
    fun onTabClose(index: Int) {
        if (index == -1) {
            // If the user clicks on close multiple times, the index may be -1 if the view is in the
            // process of being removed.
            return
        }
        val nextTab = tabListState.nextSelected(index)

        val currentTabId = currentTab?.id
        val needToSelectNextTab = tabListState[index].id == currentTabId

        compositeDisposable += model.deleteTab(tabListState[index].id)
            .observeOn(mainScheduler)
            .subscribe {
                if (needToSelectNextTab) {
                    nextTab?.id?.let {
                        selectTab(model.selectTab(it))
                        if (tabIdOpenedFromAction == currentTabId) {
                            tabIdOpenedFromAction = -1
                            navigator.backgroundBrowser()
                        }
                    } ?: run {
                        selectTab(tabModel = null)
                        navigator.closeBrowser()
                    }

                }
            }
    }

    /**
     * Call when the tab drawer is opened or closed.
     *
     * @param isOpen True if the drawer is now open, false if it is now closed.
     */
    fun onTabDrawerMoved(isOpen: Boolean) {
        isTabDrawerOpen = isOpen
    }

    /**
     * Call when the bookmark drawer is opened or closed.
     *
     * @param isOpen True if the drawer is now open, false if it is now closed.
     */
    fun onBookmarkDrawerMoved(isOpen: Boolean) {
        isBookmarkDrawerOpen = isOpen
    }

    /**
     * Called when the user clicks on the device back button or swipes to go back. Differentiated
     * from [onBackClick] which is called when the user presses the browser's back button.
     */
    fun onNavigateBack() {
        when {
            isCustomViewShowing -> {
                view?.hideCustomView()
                currentTab?.hideCustomView()
            }

            isTabDrawerOpen -> view?.closeTabDrawer()
            isBookmarkDrawerOpen -> if (currentFolder != Bookmark.Folder.Root) {
                onBookmarkMenuClick()
            } else {
                view?.closeBookmarkDrawer()
            }

            currentTab?.canGoBack() == true -> currentTab?.goBack()
            currentTab?.canGoBack() == false -> if (incognitoMode) {
                currentTab?.id?.let {
                    view?.showCloseBrowserDialog(it)
                }
            } else if (tabIdOpenedFromAction == currentTab?.id) {
                onTabClose(tabListState.indexOfCurrentTab())
            } else {
                navigator.backgroundBrowser()
            }
        }
    }

    /**
     * Called when the user presses the browser's back button.
     */
    fun onBackClick() {
        if (currentTab?.canGoBack() == true) {
            currentTab?.goBack()
        }
    }

    /**
     * Called when the user presses the browser's forward button.
     */
    fun onForwardClick() {
        if (currentTab?.canGoForward() == true) {
            currentTab?.goForward()
        }
    }

    /**
     * Call when the user clicks on the home button.
     */
    fun onHomeClick() {
        currentTab?.loadFromInitializer(mHomePageCreator)
    }

    /**
     * Call when the user clicks on the open new tab button.
     */
    fun onNewTabClick() {
        createNewTabAndSelect(mHomePageCreator, shouldSelect = true)
    }

    /**
     * Call when the user long clicks on the new tab button, indicating that they want to re-open
     * the last closed tab.
     */
    fun onNewTabLongClick() {
        compositeDisposable += model.reopenTab()
            .observeOn(mainScheduler)
            .subscribeBy { tab ->
                selectTab(model.selectTab(tab.id))
            }
    }

    /**
     * Call when the user clicks on the refresh (or stop/delete) button that is located in the
     * search bar.
     */
    fun onRefreshOrStopClick() {
        if (isSearchViewFocused) {
            view?.renderState(viewState.copy(displayUrl = ""))
            return
        }
        if (currentTab?.loadingProgress != 100) {
            currentTab?.stopLoading()
        } else {
            reload()
        }
    }

    private fun reload() {
        val currentUrl = currentTab?.url
        if (currentUrl?.isInnerUrl() == true) {
            when {
                currentUrl.isBookmarkUrl() ->
                    compositeDisposable += mBookmarkFactory.buildPage()
                        .subscribeOn(ThreadPool.getDBScheduler())
                        .observeOn(mainScheduler)
                        .subscribeBy {
                            currentTab?.reload()
                        }

                currentUrl.isHistoryUrl() ->
                    currentTab?.loadFromInitializer(historyCreator)

                currentUrl.isWebResUrl() ->
                    currentTab?.loadFromInitializer(webResCreator)

                else -> currentTab?.reload()
            }
        } else {
            currentTab?.reload()
        }
    }

    /**
     * Call when the focus state changes for the search bar.
     *
     * @param isFocused True if the view is now focused, false otherwise.
     */
    fun onSearchFocusChanged(isFocused: Boolean) {
        isSearchViewFocused = isFocused
        if (isFocused) {
            view?.updateState(
                viewState.copy(
                    sslState = SslState.None,
                    isRefresh = false,
                    displayUrl = currentTab?.url?.takeIf { !it.isInnerUrl() }.orEmpty()
                )
            )
        } else {
            view?.updateState(
                viewState.copy(
                    sslState = currentTab?.sslState ?: SslState.None,
                    isRefresh = (currentTab?.loadingProgress ?: 0) == 100,
                    displayUrl = SearchBoxModel.getDisplayContent(
                        url = currentTab?.url.orEmpty(),
                        title = currentTab?.title.orEmpty(),
                        isLoading = (currentTab?.loadingProgress ?: 0) < 100
                    )
                )
            )
        }
    }

    /**
     * Call when the user submits a search [query] to the search bar. At this point the user has
     * provided intent to search and is no longer trying to manipulate the query.
     */
    fun onSearch(query: String) {
        if (query.isEmpty()) {
            return
        }
        currentTab?.stopLoading()
        val searchUrl = SearchEngineProvider.provideSearchEngine().queryUrl + QUERY_PLACE_HOLDER
        val url = smartUrlFilter(query.trim(), true, searchUrl)
        view?.updateState(
            viewState.copy(
                displayUrl = SearchBoxModel.getDisplayContent(
                    url = url,
                    title = currentTab?.title,
                    isLoading = (currentTab?.loadingProgress ?: 0) < 100
                )
            )
        )
        currentTab?.loadUrl(url)
    }

    /**
     * Call when the user enters a [query] to look for in the current web page.
     */
    fun onFindInPage(query: String) {
        currentTab?.find(query)
        view?.updateState(viewState.copy(findInPage = query))
    }

    /**
     * Call when the user selects to move to the next highlighted word in the web page.
     */
    fun onFindNext() {
        currentTab?.findNext()
    }

    /**
     * Call when the user selects to move to the previous highlighted word in the web page.
     */
    fun onFindPrevious() {
        currentTab?.findPrevious()
    }

    /**
     * Call when the user chooses to dismiss the find in page UI component.
     */
    fun onFindDismiss() {
        currentTab?.clearFindMatches()
        view?.updateState(viewState.copy(findInPage = ""))
    }

    /**
     * Call when the user selects a search suggestion that was suggested by the search box.
     */
    fun onSearchSuggestionClicked(webPage: WebPage) {
        val url = when (webPage) {
            is HistoryEntry,
            is Bookmark.Entry -> webPage.url

            is SearchSuggestion -> webPage.title
            else -> null
        } ?: error("Other types cannot be search suggestions: $webPage")

        onSearch(url)
    }

    /**
     * Call when the user clicks on the SSL icon in the search box.
     */
    fun onSslIconClick() {
        currentTab?.sslCertificateInfo?.let {
            view?.showSslDialog(it)
        }
    }

    /**
     * Call when the user clicks on a bookmark from the bookmark list at the provided [index].
     */
    fun onBookmarkClick(index: Int) {
        when (val bookmark = viewState.bookmarks[index]) {
            is Bookmark.Entry -> {
                currentTab?.loadUrl(bookmark.url)
                view?.closeBookmarkDrawer()
            }

            Bookmark.Folder.Root -> error("Cannot click on root folder")
            is Bookmark.Folder.Entry -> {
                currentFolder = bookmark
                compositeDisposable += BookmarkDAO.getImpl()
                    .bookmarksAndFolders(folder = bookmark)
                    .subscribeOn(databaseScheduler)
                    .observeOn(mainScheduler)
                    .subscribe { list ->
                        view?.updateState(viewState.copy(bookmarks = list, isRootFolder = false))
                    }
            }
        }
    }

    private fun BookmarkDAO.bookmarksAndFolders(folder: Bookmark.Folder): Single<List<Bookmark>> =
        getBookmarksFromFolderSorted(folder = folder.title)
            .concatWith(Single.defer {
                if (folder == Bookmark.Folder.Root) {
                    getFoldersSorted()
                } else {
                    Single.just(emptyList())
                }
            })
            .toList()
            .map(MutableList<List<Bookmark>>::flatten)

    /**
     * Call when the user long presses on a bookmark in the bookmark list at the provided [index].
     */
    fun onBookmarkLongClick(index: Int) {
        when (val item = viewState.bookmarks[index]) {
            is Bookmark.Entry -> view?.showBookmarkOptionsDialog(item)
            is Bookmark.Folder.Entry -> view?.showFolderOptionsDialog(item)
            Bookmark.Folder.Root -> Unit // Root is not clickable
        }
    }

    /**
     * Call when the user clicks on the page tools button.
     */
    fun onToolsClick() {

    }

    /**
     * Call when the user chooses to toggle the desktop user agent on/off.
     */
    fun onToggleDesktopAgent() {
        currentTab?.toggleDesktopAgent()
        currentTab?.reload()
    }

    /**
     * Call when the user clicks on the star icon to add a bookmark for the current page or remove the existing one.
     */
    fun onStarClick() {
        val url = currentTab?.url ?: return
        val title = currentTab?.title.orEmpty()
        if (url.isInnerUrl()) {
            return
        }
        compositeDisposable += BookmarkDAO.getImpl().isBookmark(url)
            .flatMapMaybe { stared ->
                if (stared) {
                    BookmarkDAO.getImpl().deleteBookmark(Bookmark.Entry(url, title, 0, Bookmark.Folder.Root, "", "")).toMaybe()
                } else {
                    Maybe.empty()
                }
            }
            .doOnComplete(::showAddBookmarkDialog)
            .flatMapSingle { BookmarkDAO.getImpl().bookmarksAndFolders(folder = currentFolder) }
            .subscribeOn(databaseScheduler)
            .observeOn(mainScheduler)
            .subscribeBy { list ->
                this.view?.updateState(viewState.copy(bookmarks = list, isBookmarked = BookmarkDAO.getImpl().existsBookmark(url)))
            }
    }

    private fun showAddBookmarkDialog() {
        compositeDisposable += BookmarkDAO.getImpl().getFolderNames()
            .subscribeOn(databaseScheduler)
            .observeOn(mainScheduler)
            .subscribeBy {
                view?.showAddBookmarkDialog(currentTab?.title.orEmpty(), currentTab?.url.orEmpty(), it)
            }
    }

    /**
     * Call when the user confirms the details for adding a bookmark.
     *
     * @param title The title of the bookmark.
     * @param url The URL of the bookmark.
     * @param folder The name of the folder the bookmark is in.
     */
    fun onBookmarkConfirmed(title: String, url: String, folder: String) {
        compositeDisposable += BookmarkDAO.getImpl().addBookmarkIfNotExists(
            Bookmark.Entry(
                url = url,
                title = title,
                position = 0,
                folder = folder.asFolder(),
                icon = "",
                addDate = ""
            )
        ).flatMap { BookmarkDAO.getImpl().bookmarksAndFolders(folder = currentFolder) }
            .subscribeOn(databaseScheduler)
            .observeOn(mainScheduler)
            .subscribeBy { list ->
                this.view?.updateState(viewState.copy(bookmarks = list, isBookmarked = true))
            }
    }

    /**
     * Call when the user confirms the details when editing a bookmark.
     *
     * @param title The title of the bookmark.
     * @param url The URL of the bookmark.
     * @param folder The name of the folder the bookmark is in.
     */
    fun onBookmarkEditConfirmed(title: String, url: String, folder: String) {
        compositeDisposable += BookmarkDAO.getImpl().editBookmark(
            oldBookmark = Bookmark.Entry(
                url = url,
                title = "",
                position = 0,
                folder = Bookmark.Folder.Root,
                icon = "",
                addDate = ""
            ),
            newBookmark = Bookmark.Entry(
                url = url,
                title = title,
                position = 0,
                folder = folder.asFolder(),
                icon = "",
                addDate = ""
            )
        ).andThen(BookmarkDAO.getImpl().bookmarksAndFolders(folder = currentFolder))
            .subscribeOn(databaseScheduler)
            .observeOn(mainScheduler)
            .subscribeBy { list ->
                this.view?.updateState(viewState.copy(bookmarks = list))
                if (currentTab?.url?.isBookmarkUrl() == true) {
                    reload()
                }
            }
    }

    /**
     * Call when the user confirms a name change to an existing folder.
     *
     * @param oldTitle The previous title of the folder.
     * @param newTitle The new title of the folder.
     */
    fun onBookmarkFolderRenameConfirmed(oldTitle: String, newTitle: String) {
        compositeDisposable += BookmarkDAO.getImpl().renameFolder(oldTitle, newTitle)
            .andThen(BookmarkDAO.getImpl().bookmarksAndFolders(folder = currentFolder))
            .subscribeOn(databaseScheduler)
            .observeOn(mainScheduler)
            .subscribe { list ->
                this.view?.updateState(viewState.copy(bookmarks = list))
                if (currentTab?.url?.isBookmarkUrl() == true) {
                    reload()
                }
            }
    }

    /**
     * Call when the user clicks on a menu [option] for the provided [bookmark].
     */
    fun onBookmarkOptionClick(bookmark: Bookmark.Entry, @BrowserConst.Bookmark option: Int) {
        when (option) {
            BrowserConst.BOOKMARK_NEW_TAB -> createNewTabAndSelect(UrlCreator(bookmark.url), shouldSelect = true)
            BrowserConst.BOOKMARK_BACKGROUND_TAB -> createNewTabAndSelect(UrlCreator(bookmark.url), shouldSelect = false)
            BrowserConst.BOOKMARK_INCOGNITO_TAB -> navigator.launchIncognitoWindow(bookmark.url)
            BrowserConst.BOOKMARK_SHARE -> navigator.sharePage(url = bookmark.url, title = bookmark.title)
            BrowserConst.BOOKMARK_COPY_LINK -> navigator.copyPageLink(bookmark.url)
            BrowserConst.BOOKMARK_REMOVE ->
                compositeDisposable += BookmarkDAO.getImpl().deleteBookmark(bookmark)
                    .flatMap { BookmarkDAO.getImpl().bookmarksAndFolders(folder = currentFolder) }
                    .subscribeOn(databaseScheduler)
                    .observeOn(mainScheduler)
                    .subscribe { list ->
                        view?.updateState(viewState.copy(bookmarks = list))
                        if (currentTab?.url?.isBookmarkUrl() == true) {
                            reload()
                        }
                    }

            BrowserConst.BOOKMARK_EDIT ->
                compositeDisposable += BookmarkDAO.getImpl().getFolderNames()
                    .subscribeOn(databaseScheduler)
                    .observeOn(mainScheduler)
                    .subscribeBy { folders ->
                        view?.showEditBookmarkDialog(
                            bookmark.title,
                            bookmark.url,
                            bookmark.folder.title,
                            folders
                        )
                    }
        }
    }

    /**
     * Call when the user clicks on a menu [option] for the provided [folder].
     */
    fun onFolderOptionClick(folder: Bookmark.Folder, @BrowserConst.Folder option: Int) {
        when (option) {
            BrowserConst.FOLDER_RENAME -> view?.showEditFolderDialog(folder.title)
            BrowserConst.FOLDER_REMOVE ->
                compositeDisposable += BookmarkDAO.getImpl().deleteFolder(folder.title)
                    .andThen(BookmarkDAO.getImpl().bookmarksAndFolders(folder = currentFolder))
                    .subscribeOn(databaseScheduler)
                    .observeOn(mainScheduler)
                    .subscribe { list ->
                        view?.updateState(viewState.copy(bookmarks = list))
                        if (currentTab?.url?.isBookmarkUrl() == true) {
                            reload()
                            currentTab?.goBack()
                        }
                    }
        }
    }

    /**
     * Call when the user clicks on a menu [option] for the provided [historyEntry].
     */
    fun onHistoryOptionClick(
        historyEntry: HistoryEntry,
        @BrowserConst.History option: Int
    ) {
        when (option) {
            BrowserConst.HISTORY_NEW_TAB -> createNewTabAndSelect(UrlCreator(historyEntry.url), shouldSelect = true)
            BrowserConst.HISTORY_BACKGROUND_TAB -> createNewTabAndSelect(UrlCreator(historyEntry.url), shouldSelect = false)
            BrowserConst.HISTORY_INCOGNITO_TAB -> navigator.launchIncognitoWindow(historyEntry.url)
            BrowserConst.HISTORY_SHARE -> navigator.sharePage(url = historyEntry.url, title = historyEntry.title)
            BrowserConst.HISTORY_COPY_LINK -> navigator.copyPageLink(historyEntry.url)
            BrowserConst.HISTORY_REMOVE ->
                compositeDisposable += HistoryDAO.getImpl().deleteHistoryEntry(historyEntry.url)
                    .subscribeOn(databaseScheduler)
                    .observeOn(mainScheduler)
                    .subscribeBy {
                        if (currentTab?.url?.isHistoryUrl() == true) {
                            reload()
                        }
                    }
        }
    }

    /**
     * Call when the user clicks on the tab count button (or home button in desktop mode, or
     * incognito icon in incognito mode).
     */
    fun onTabCountViewClick() {
        view?.openTabDrawer()
    }

    /**
     * Call when the user clicks on the tab menu located in the tab drawer.
     */
    fun onTabMenuClick() {
        currentTab?.let {
            view?.showCloseBrowserDialog(it.id)
        }
    }

    /**
     * Call when the user clicks on the bookmark menu (star or back arrow) located in the bookmark
     * drawer.
     */
    fun onBookmarkMenuClick() {
        if (currentFolder != Bookmark.Folder.Root) {
            currentFolder = Bookmark.Folder.Root
            compositeDisposable += BookmarkDAO.getImpl()
                .bookmarksAndFolders(folder = Bookmark.Folder.Root)
                .subscribeOn(databaseScheduler)
                .observeOn(mainScheduler)
                .subscribeBy { list ->
                    view?.updateState(viewState.copy(bookmarks = list, isRootFolder = true))
                }
        }
    }

    /**
     * Call when the user long presses anywhere on the web page with the provided tab [id].
     */
    fun onPageLongPress(id: Int, longPress: LongPress) {
        val pageUrl = model.tabsList.find { it.id == id }?.url
        if (pageUrl?.isInnerUrl() == true) {
            val url = longPress.targetUrl ?: return
            if (pageUrl.isBookmarkUrl()) {
                if (url.isBookmarkUrl()) {
                    val filename = requireNotNull(longPress.targetUrl.toUri().lastPathSegment) {
                        "Last segment should always exist for bookmark file"
                    }
                    val folderTitle = filename.substring(
                        0,
                        filename.length - BOOKMARKS_FILE_NAME.length - 1
                    )
                    view?.showFolderOptionsDialog(folderTitle.asFolder())
                } else {
                    compositeDisposable += BookmarkDAO.getImpl().findBookmarkForUrl(url)
                        .subscribeOn(databaseScheduler)
                        .observeOn(mainScheduler)
                        .subscribeBy {
                            view?.showBookmarkOptionsDialog(it)
                        }
                }
            } else if (pageUrl.isHistoryUrl()) {
                compositeDisposable += HistoryDAO.getImpl().findHistoryEntriesContaining(url)
                    .subscribeOn(databaseScheduler)
                    .observeOn(mainScheduler)
                    .subscribeBy { entries ->
                        entries.firstOrNull()?.let {
                            view?.showHistoryOptionsDialog(it)
                        } ?: view?.showHistoryOptionsDialog(HistoryEntry(url = url, title = ""))
                    }

            }
        } else {
            when (longPress.hitCategory) {
                LongPress.Category.IMAGE -> view?.showImageLongPressDialog(longPress)
                LongPress.Category.LINK -> view?.showLinkLongPressDialog(longPress)
                LongPress.Category.UNKNOWN -> Unit // Do nothing
            }
        }
    }

    /**
     * Call when the user selects an option from the close browser menu that can be invoked by long
     * pressing on individual tabs.
     */
    fun onCloseBrowserEvent(id: Int, @CloseTab closeTabEvent: Int) {
        when (closeTabEvent) {
            BrowserConst.CLOSE_TAB_AFTER -> model.tabsList
                .filter { tabListState.tabIndexForId(it.id) > tabListState.tabIndexForId(id) }
                .toObservable()
                .flatMapCompletable { model.deleteTab(it.id) }
                .subscribeOn(mainScheduler)
                .subscribe()

            BrowserConst.CLOSE_TAB_OTHERS -> model.tabsList
                .filter { it.id != id }
                .toObservable()
                .flatMapCompletable { model.deleteTab(it.id) }
                .subscribeOn(mainScheduler)
                .subscribe()

            BrowserConst.CLOSE_TAB_ALL ->
                compositeDisposable += model.deleteAllTabs().subscribeOn(mainScheduler)
                    .subscribeBy(onComplete = navigator::closeBrowser)
        }
    }

    /**
     * Call when the user long presses on a link within the web page and selects what they want to
     * do with that link.
     */
    fun onLinkLongPressEvent(longPress: LongPress, @BrowserConst.LinkLongPress linkLongPressEvent: Int) {
        when (linkLongPressEvent) {

            BrowserConst.LINK_LONG_PRESS_NEW_TAB -> longPress.targetUrl?.let {
                createNewTabAndSelect(UrlCreator(it), shouldSelect = true)
            }

            BrowserConst.LINK_LONG_PRESS_BACKGROUND_TAB -> longPress.targetUrl?.let {
                createNewTabAndSelect(UrlCreator(it), shouldSelect = false)
            }

            BrowserConst.LINK_LONG_PRESS_INCOGNITO_TAB -> longPress.targetUrl?.let(navigator::launchIncognitoWindow)
            BrowserConst.LINK_LONG_PRESS_SHARE -> longPress.targetUrl?.let { navigator.sharePage(url = it, title = null) }

            BrowserConst.LINK_LONG_PRESS_COPY_LINK -> longPress.targetUrl?.let(navigator::copyPageLink)
        }
    }

    /**
     * Call when the user long presses on an image within the web page and selects what they want to
     * do with that image.
     */
    fun onImageLongPressEvent(
        longPress: LongPress,
        @BrowserConst.ImageLongPress option: Int
    ) {
        when (option) {

            BrowserConst.IMAGE_LONG_PRESS_NEW_TAB -> longPress.targetUrl?.let {
                createNewTabAndSelect(UrlCreator(it), shouldSelect = true)
            }

            BrowserConst.IMAGE_LONG_PRESS_BACKGROUND_TAB -> longPress.targetUrl?.let {
                createNewTabAndSelect(UrlCreator(it), shouldSelect = false)
            }

            BrowserConst.IMAGE_LONG_PRESS_INCOGNITO_TAB -> longPress.targetUrl?.let(navigator::launchIncognitoWindow)
            BrowserConst.IMAGE_LONG_PRESS_SHARE -> longPress.targetUrl?.let { navigator.sharePage(url = it, title = null) }
            BrowserConst.IMAGE_LONG_PRESS_COPY_LINK -> longPress.targetUrl?.let(navigator::copyPageLink)
            BrowserConst.IMAGE_LONG_PRESS_DOWNLOAD -> navigator.download(
                DownloadRequest(
                    url = longPress.targetUrl.orEmpty(),
                    userAgent = null,
                    contentDisposition = "attachment",
                    mimeType = null,
                    contentLength = 0
                )
            )
        }
    }

    /**
     * Call when the user has selected a file from the file chooser to upload.
     */
    fun onFileChooserResult(activityResult: ActivityResult) {
        currentTab?.handleFileChooserResult(activityResult)
    }

    private fun BrowserContract.View?.updateState(state: BrowserViewState) {
        viewState = state
        this?.renderState(viewState)
    }

    private fun BrowserContract.View?.updateTabs(tabs: List<TabViewState>) {
        tabListState = tabs
        this?.renderTabs(tabListState)
    }
}
